#include "plugin.manager.h"
#include "graph.h"
#include "plugins/plugin.h"
//#include "compute.thread.h"
#include "misc.h"

#include <QtCore/QtCore>
#include <QFileDialog>
#include <QMessageBox>
#include <queue>
#include <memory>
#include "QFntLibrary.h"
#ifdef BUILD_MINGW
#define SO_TEMPLATE PACKAGE_NAME "-plugin-*.dll"
#else
#ifdef BUILD_APPLE
#define SO_TEMPLATE PACKAGE_NAME "-plugin-*.dylib"
#else
#define SO_TEMPLATE PACKAGE_NAME "-plugin-*.so"
#endif
#endif

void Algorithm::updateCube(int ch, const CubeRef& c, const std::pair<double, double>& xfunc) {
	_cubes[ch]=c;
	_xfuncs[ch]=xfunc;
	cubeUpdated(ch);
}
void Algorithm::updateXfunc(int ch, const std::pair<double, double>& xfunc) {
	_xfuncs[ch]=xfunc;
	xfuncUpdated(ch);
}
void Algorithm::updatePosition() {
	positionChanged();
}
void Algorithm::setCallback(bool (*f)(Algorithm*, void*), void* d) {
	_callback_func=f;
	_callback_data=d;
}
void Algorithm::setInfo(const AlgorithmInfo* i) {
	_info=i;
}
const AlgorithmInfo* Algorithm::info() const {
	return _info;
}

void GraphReader::setupCube(int ch, const CubeRef& c, const std::pair<double, double>& x) {
	_cubes[ch]=c;
	_xfuncs[ch]=x;
}
void GraphReader::setupCubeAnnot(const CubeRef& c, const std::vector<AnnotItem>* a) {
	_cube_annot=c;
	if(a) {
		_annots_ptr=a->data();
		_annots_num=a->size();
	} else {
		_annots_ptr=nullptr;
		_annots_num=0;
	}
}


struct AlgFindDumb final: public AlgFind {
	static AlgFind* creater() { return new AlgFindDumb{}; }
	void setParams(const std::vector<ParamValue>& params) override { }
	std::vector<ParamValue> getParams() const override { return {}; }
	AlgFindDumb() : AlgFind() { }
	void find(const Position& p, Graph gr, Path* pth) override { pth->points.clear(); }
};

struct AlgConnDumb : public AlgConn {
	static AlgConn* creater() { return new AlgConnDumb{}; }
	void setParams(const std::vector<ParamValue>& params) override { }
	std::vector<ParamValue> getParams() const override { return {}; }
	AlgConnDumb() : AlgConn() { }
	void connect(const Position& p0, const Position& p1, Graph gr, Path* pth) override { pth->points.clear(); }
};

struct AlgRefineDumb : public AlgRefine {
	static AlgRefine* creater() { return new AlgRefineDumb{}; }
	void setParams(const std::vector<ParamValue>& params) override { }
	std::vector<ParamValue> getParams() const override { return {}; }
	AlgRefineDumb() : AlgRefine() { }
	void refine(Graph gr, Path* pth) override { }
};

struct GraphReaderSummary: public GraphReader {
	GraphReaderSummary(): GraphReader() { }
	~GraphReaderSummary() override { }
	void read(Graph gr, QWidget* par) override {
		size_t nnode=0;
		for(auto e: gr.edges()) {
			nnode+=e.points().size();
		}
		auto text=QString{"Number of neurons: %1\nNumber of edges: %2\nNumber of vertices: %3\nNumber of nodes: %4"}.arg(gr.trees().size()).arg(gr.edges().size()).arg(gr.vertices().size()).arg(nnode-gr.edges().size()*2+gr.vertices().size());
		QMessageBox::information(par, "Summary", text, QMessageBox::Ok, QMessageBox::Ok);
	}
	static GraphReaderInfo info;
	static GraphReader* creater() { return new GraphReaderSummary{}; }
};
struct GraphExportSwc: public GraphReader {
	GraphExportSwc(): GraphReader() { }
	~GraphExportSwc() override { }
	void read(Graph gr, QWidget* par) override {
		QVector<size_t> idxs{};
		for(size_t i=0; i<gr.trees().size(); i++) {
			if(gr.trees()[i].selected()) {
				idxs.push_back(i);
			}
		}
		if(idxs.size()<=0) {
			showWarning("Read session", "No neuron is selected.", par);
			return;
		}
		for(auto i: idxs) {
			if(!gr.trees()[i].completed()) {
				showWarning("Read session", "One of the selected neurons is incomplete or incorrect.", par);
				return;
			}
		}
		QFileDialog dlg{par, "Please choose a SWC file to save.", "", ""};
		dlg.setAcceptMode(QFileDialog::AcceptSave);
		dlg.setDefaultSuffix("swc");
		dlg.setFileMode(QFileDialog::AnyFile);
		dlg.setNameFilter("SWC file (*.swc)");
		if(dlg.exec()!=QDialog::Accepted) {
			return;
		}
		try {
			QFile file(dlg.selectedFiles()[0]);
			if(!file.open(QIODevice::WriteOnly)) {
				throwError("Cannot open file");
			}
			QTextStream fs{&file};
			fs.setRealNumberNotation(QTextStream::SmartNotation);
			fs.setRealNumberPrecision(13);
			fs<<"# Tracing result of "<<idxs.size()<<" neuron(s)\n";
			fs<<"## powered by " PACKAGE_NAME "<" PACKAGE_URL ">\n";
			if(fs.status()!=QTextStream::Ok)
				throwError("Failed to write header");
			std::vector<int64_t> v2nid(gr.vertices().size(), -1);
			std::vector<bool> e2tag(gr.edges().size(), false);
			int64_t nid=1;
			std::queue<Vertex> que{};
			for(auto i: idxs) {
				auto n=gr.trees()[i];
				auto s=n.root();
				auto& pos=s.point();
				fs<<nid<<" 1 "<<pos.x()<<" "<<pos.y()<<" "<<pos.z()<<" "<<pos.r()<<" -1\n";
				if(fs.status()!=QTextStream::Ok)
					throwError("Failed to write line");
				v2nid[s.index()]=nid++;
				que.push(s);
			}
			while(!que.empty()) {
				auto v=que.front();
				que.pop();
				auto nid_par=v2nid[v.index()];
				for(auto& ep: v.neighbors()) {
					auto e=ep.first;
					if(e2tag[e.index()])
						continue;
					if(!ep.second) {
						auto pnid=nid_par;
						for(size_t j=e.length(); j>1; j--) {
							auto& pos=e.points()[j-2];
							fs<<nid<<" ";
							if(pos.m) {
								fs<<pos.m+9;
							} else {
								fs<<e.type();
							}
							fs<<" "<<pos.x()<<" "<<pos.y()<<" "<<pos.z()<<" "<<pos.r()<<" "<<pnid<<"\n";
							if(fs.status()!=QTextStream::Ok)
								throwError("Failed to write line");
							pnid=nid++;
						}
						e2tag[e.index()]=true;
						auto nv=e.leftVertex();
						v2nid[nv.index()]=pnid;
						que.push(nv);
					} else {
						auto pnid=nid_par;
						for(size_t j=1; j<e.length(); j++) {
							auto& pos=e.points()[j];
							fs<<nid<<" ";
							if(pos.m) {
								fs<<pos.m+9;
							} else {
								fs<<e.type();
							}
							fs<<" "<<pos.x()<<" "<<pos.y()<<" "<<pos.z()<<" "<<pos.r()<<" "<<pnid<<"\n";
							if(fs.status()!=QTextStream::Ok)
								throwError("Failed to write line");
							pnid=nid++;
						}
						e2tag[e.index()]=true;
						auto nv=e.rightVertex();
						v2nid[nv.index()]=pnid;
						que.push(nv);
					}
				}
			}

		} catch(const std::exception& e) {
			showWarning("Failed to export SWC", e.what(), par);
			return;
		}
	}
	static GraphReaderInfo info;
	static GraphReader* creater() { return new GraphExportSwc{}; }
};

static void checkParamSpecs(const std::vector<ParamSpec>& specs) {
	for(auto& spec: specs) {
		switch(spec.type()) {
			case ParamType::INT:
				if(spec.asInt().minimum>spec.asInt().maximum)
					throwError("Wrong range.");
				if(spec.asInt().initial<spec.asInt().minimum
						|| spec.asInt().initial>spec.asInt().maximum)
					throwError("Wrong default value.");
				break;
			case ParamType::FLOAT:
				if(spec.asFloat().minimum>spec.asFloat().maximum)
					throwError("Wrong range.");
				if(spec.asFloat().initial<spec.asFloat().minimum
						|| spec.asFloat().initial>spec.asFloat().maximum)
					throwError("Wrong default value.");
				break;
			case ParamType::ENUM:
				if(spec.asEnum().choices.size()<=0)
					throwError("Wrong range size.");
				{
					if(spec.asEnum().initial<0 || static_cast<size_t>(spec.asEnum().initial)>=spec.asEnum().choices.size())
						throwError("Wrong default value.");
				}
				break;
			default:
				throwError("Unknown param type.");
		}
	}
}

class PluginManagerPriv {
public:
	QList<const Plugin*> plugins;
	QList<QPair<const AlgorithmInfo*, const Plugin*>> algs;
	QList<const AlgInfo<AlgFind>*> algsFind;
	QList<const AlgInfo<AlgConn>*> algsConn;
	QList<const AlgInfo<AlgRefine>*> algsRefine;
	QList<QPair<const GraphReaderInfo*, const Plugin*>> readers;
	QVector<QLibrary*> libraries;

	PluginManagerPriv():
		plugins{},
		algs{}, algsFind{}, algsConn{}, algsRefine{},
		readers{},
		libraries{}
	{
		auto algFindNull=new AlgInfo<AlgFind>{"Dumb", 0,
			"Dummy algorithm that does nothing",
			{ },
			AlgFindDumb::creater};
		auto algConnNull=new AlgInfo<AlgConn>{"Dumb", 0,
			"Dummy algorithm that does nothing",
			{ },
			AlgConnDumb::creater};
		auto algRefineNull=new AlgInfo<AlgRefine>{"Dumb", 0,
			"Dummy algorithm that does nothing",
			{ },
			AlgRefineDumb::creater};

		auto readerSummary=new GraphReaderInfo{"Summary", 100,
			"Show basic summary of the traced data.",
			GraphReaderSummary::creater};

		auto readerExportSwc=new GraphReaderInfo{"Export to SWC", 99,
			"Export selected neurons to SWC file.",
			GraphExportSwc::creater};

		checkAndAddAlg(algFindNull, nullptr);
		checkAndAddAlg(algConnNull, nullptr);
		checkAndAddAlg(algRefineNull, nullptr);
		checkAndAddReader(readerSummary, nullptr);
		checkAndAddReader(readerExportSwc, nullptr);
		auto pluginPaths=pathOfFntPlugins();
		for(auto& pth: pluginPaths) {
			printMessage("Loading plugins from: ", pth);
			QDir dir{pth};
			for(auto& p: dir.entryList({SO_TEMPLATE})) {
				auto pp=dir.absoluteFilePath(p);
				auto lib=new QFntLibrary{pp};
				libraries.push_back(lib);
				if(!lib->load()) {
					showWarning("Failed to load "+pp, lib->errorString());
					continue;
				}
				auto ptr=lib->resolve("fnt_plugin");
				if(!ptr) {
					showWarning("Failed to resolve fnt_plugin in "+pp, lib->errorString());
					continue;
				}
				auto plugin=reinterpret_cast<const Plugin*>(ptr);
				if(plugin->fnt_version!=QString{PACKAGE_VERSION}) {
					showWarning("Load plugins", "Version mismatch in "+pp+": "+plugin->fnt_version);
					continue;
				}
				std::vector<AlgorithmInfo*> list;
				std::vector<GraphReaderInfo*> readers;
				plugin->load_func(list, readers);
				for(auto alg: list) {
					checkAndAddAlg(alg, plugin);
				}
				for(auto alg: readers) {
					checkAndAddReader(alg, plugin);
				}
				plugins.push_back(plugin);
				printMessage("Loaded: ", p);
			}
		}
		std::sort(algsFind.begin(), algsFind.end(), [](const AlgInfo<AlgFind>* a, const AlgInfo<AlgFind>* b)->bool { return a->priority>b->priority; });
		std::sort(algsConn.begin(), algsConn.end(), [](const AlgInfo<AlgConn>* a, const AlgInfo<AlgConn>* b)->bool { return a->priority>b->priority; });
		std::sort(algsRefine.begin(), algsRefine.end(), [](const AlgInfo<AlgRefine>* a, const AlgInfo<AlgRefine>* b)->bool { return a->priority>b->priority; });
		std::sort(algs.begin(), algs.end(), [](const QPair<const AlgorithmInfo*, const Plugin*>& a, const QPair<const AlgorithmInfo*, const Plugin*>& b)->bool { return a.first->priority>b.first->priority; });
		std::sort(readers.begin(), readers.end(), [](const QPair<const GraphReaderInfo*, const Plugin*>& a, const QPair<const GraphReaderInfo*, const Plugin*>& b)->bool { return a.first->priority>b.first->priority; });
	}

	void checkAndAddAlg(const AlgorithmInfo* info, const Plugin* plugin) {
		try {
			checkParamSpecs(info->paramSpecs);
			auto algfind=dynamic_cast<const AlgInfo<AlgFind>*>(info);
			auto algconn=dynamic_cast<const AlgInfo<AlgConn>*>(info);
			auto algrefine=dynamic_cast<const AlgInfo<AlgRefine>*>(info);
			int ncast=0;
			if(algfind) {
				auto i=std::find_if(algsFind.begin(), algsFind.end(), [info](const AlgorithmInfo* p) { return p->name==info->name; });
				if(i!=algsFind.end()) {
					throwError("Algorithm name conflict");
				}
				ncast++;
			}
			if(algconn) {
				auto i=std::find_if(algsConn.begin(), algsConn.end(), [info](const AlgorithmInfo* p) { return p->name==info->name; });
				if(i!=algsConn.end()) {
					throwError("Algorithm name conflict");
				}
				ncast++;
			}
			if(algrefine) {
				auto i=std::find_if(algsRefine.begin(), algsRefine.end(), [info](const AlgorithmInfo* p) { return p->name==info->name; });
				if(i!=algsRefine.end()) {
					throwError("Algorithm name conflict");
				}
				ncast++;
			}
			if(ncast!=1)
				throwError("Algorithm type error");
			algs.push_back({info, plugin});
			if(algfind) {
				algsFind.push_back(algfind);
			}
			if(algconn) {
				algsConn.push_back(algconn);
			}
			if(algrefine) {
				algsRefine.push_back(algrefine);
			}
		} catch(const std::exception& e) {
			showWarning(QString{"Failed to add algorithm '"}+info->name+"'", e.what());
		}
	}
	void checkAndAddReader(const GraphReaderInfo* info, const Plugin* plugin) {
		try {
			auto i=std::find_if(readers.begin(), readers.end(), [info](const QPair<const GraphReaderInfo*, const Plugin*>& p) { return p.first->name==info->name; });
			if(i!=readers.end()) {
				throwError("Algorithm name conflict");
			}
			readers.push_back({info, plugin});
		} catch(const std::exception& e) {
			showWarning(QString{"Failed to add graph reader '"}+info->name+"'", e.what());
		}
	}
	~PluginManagerPriv() {
		for(auto& p: algs) {
			delete p.first;
		}
		for(auto& p: readers) {
			delete p.first;
		}
		for(auto p: plugins) {
			p->unload_func();
		}
		for(auto p: libraries) {
			p->unload();
			delete p;
		}
	}
};

PluginManager* PluginManager::_instance{nullptr};

PluginManager::PluginManager():
	priv{new PluginManagerPriv{}}
{
}
PluginManager::~PluginManager() {
	delete priv;
}

const QList<const Plugin*>& PluginManager::plugins() const {
	return priv->plugins;
}

const QList<QPair<const AlgorithmInfo*, const Plugin*>>& PluginManager::algs() const {
	return priv->algs;
}
const QList<const AlgInfo<AlgFind>*>& PluginManager::algsFind() const {
	return priv->algsFind;
}
const QList<const AlgInfo<AlgConn>*>& PluginManager::algsConn() const {
	return priv->algsConn;
}
const QList<const AlgInfo<AlgRefine>*>& PluginManager::algsRefine() const {
	return priv->algsRefine;
}

const QList<QPair<const GraphReaderInfo*, const Plugin*>>& PluginManager::graphReaders() const {
	return priv->readers;
}

